Let’s examine how various SQL Server database
objects can be implemented using managed code. You can use managed code
to implement the following objects in SQL Server:
When
creating stored procedures and functions you can accept input
parameters and use output parameters to return data. You can also use
return values for functions and stored procedures. When creating a CLR
stored procedure, it can return nothing or return an integer
representing the return code. Functions can return a single value of
any type. When creating your CLR objects you can access SQL Server data
from the calling context. This is known as using the context connection.
The context connection provides you with access to the immediate
execution environment, in other words, login credentials and selected
database.
No matter what database object you choose to create, you are likely to use CLR integration custom attributes.
What are custom attributes? In the context of .NET Framework
development, custom attributes are keywords that mark a certain piece
of code. The execution environment (in this case SQL Server) examines
the attributes and provides services to your code based on these
attributes. Although you don’t need to
know all the attributes that can be applied to each type of CLR object,
it is worth understanding the concept of attributes. For example, the SqlFunction attribute indicates that the procedure marked with it is a SQL Server function. For the SqlFunction attribute you can specify multiple properties like DataAccess, Name, and TableDefinition. In this particular example, the DataAccessTableDefinition property defines the columns in the table returned by a table-valued function.
property specifies whether the function will read data from the SQL
Server context connection, or will not access SQL Server data at all;
the Name property is the name that will be used to register the
function with SQL Server; and the
Tip
You don’t need to memorize the list of attributes and their available properties. The one key attribute to remember is DataAccess. This attribute can be set to DataAccessKind.None or DataAccessKind.Read. Remember that you must specify DataAccess=DataAccessKind.Read
if your CLR object has to execute queries and read data from the
context connection. Examples later in this chapter show how this is
implemented.
Creating CLR Functions
As
explained earlier in this book, user-defined functions are compiled
routines that usually perform calculations and return a single value.
Functions can be defined using Transact-SQL or any .NET language. Let’s
take a brief tour of creating functions using managed code. Note that
while the examples will be presented in C# and Visual Basic .NET, you
can create them using any .NET language.
When
creating a function you should first determine whether it is a scalar
or a table-valued function. Scalar functions return a single value of a
SQL data type like bit or int. Table-valued functions return an
expression that can be treated by the calling code as a SQL table
comprising of columns and rows. All CLR-integrated user-defined
functions are implemented as static methods of a class that is hosted
by an assembly (see “Understanding Classes, Assemblies, Namespaces, and
the GAC” earlier in this chapter). You can use the SqlFunction custom attribute to specify additional information about your function. Examples 1 and 2 show a very simple function named SayHello that returns the value “Hello world!”
Example 1. C#
using System; using System.Data; using System.Data.SqlClient; using System.Data.SqlTypes; using Microsoft.SqlServer.Server; public class UserDefinedFunctions { [Microsoft.SqlServer.Server.SqlFunction(DataAccess=DataAccessKind.None)] public static SqlString SayHello() { return new SqlString("Hello world!"); } }
|
Example 2. Visual Basic .NET
Imports System Imports System.Data Imports System.Data.SqlClient Imports System.Data.SqlTypes Imports Microsoft.SqlServer.Server Public Class UserDefinedFunctions <Microsoft.SqlServer.Server.SqlFunction()> _ Public Shared Function SayHello() As SqlString Return New SqlString("Hello world!") End Function End Class
|
For
those who are new to .NET programming, let’s go through the elements of
this code. It is worthy of mention that the two examples are absolutely
equivalent, and do the same thing—return the value “Hello world!” when
called. If you remember when we discussed managed code, both examples
will compile to exactly the same set of MSIL statements.
The first set of lines are the using or imports directives. These directives tell the class that it will be using objects from the namespaces mentioned, including the Microsoft.SqlServer.Server namespace. This is true—we will be using the SqlFunction custom attribute that belongs in the Microsoft.SqlServer.Server namespace. We will also use the SqlString data type that belongs in the System.Data.SqlTypes namespace.
The second line declares the class. In this case, the class is called UserDefined-Functions. Note that the class doesn’t have to be called UserDefinedFunctions; you could use any name—for example, MyFunctions or SqlStuff.
Remember that classes are individual logical entities and have
properties and methods. Notice that the class end needs to be specified
by a close brace (}) in C# or the keywords End Class in Visual Basic .NET.
Once
the class is declared, we implement the actual SQL function as a method
of our class. To specify that the method should be registered as a SQL
function we use the SqlFunction custom attribute. We also specify that the SqlFunction attribute has its DataAccess property set to DataAccess.None—our function will not be accessing SQL Server data.
The method that is marked as SqlFunction is called SayHello. It is marked as public, meaning that it can be accessed externally—for example, from SQL Server. It is also marked as static in C# or Shared
in Visual Basic .NET. These keywords indicate that the method can be
called directly, without creating an instance of the class. You must
mark methods that will become SQL Server functions, stored procedures,
and aggregates as static/Shared
for them to work properly. Our method takes no parameters as it has two
empty round brackets after it, and returns a value of type SqlString (text). Inside the body of the method we simply use the returnSqlString containing the value “Hello world!” When this function is called, it will return “Hello world!” keyword to return a new
Tip
Remember that all SQL objects are written as methods of a class. They must be marked as static in C# or Shared in Visual Basic .NET. They must also be marked as public, or else the SQL Server execution environment will not be able to access them.
Table 1 lists all properties applicable to the SqlFunction
custom attribute. You don’t need to memorize all of these
properties—the table is provided for you to get an idea of what type of
control is available for your user-defined CLR functions.
Table 1. SqlFunction Custom Attribute Properties
Property | Explanation |
---|
IsDeterministic | True
if the function will always return the same value, if called with the
same parameters. False if the function can return different values when
called with the same parameters. An example of a deterministic function
is the SayHello function. An example of a nondeterministic function is a function that will use database data that varies. |
DataAccess | Specifies whether the function will read data from the context SQL Server connection. Acceptable values are DataAccessKind.None and DataAccessKind.Read. |
SystemDataAccess | Specifies whether the function will require access to system catalogs or system tables in SQL Server—for example, sys.object. Acceptable values are System DataAccessKind.None and SystemDataAccessKind. Read. |
IsPrecise | Specifies
whether the function will be performing imprecise computation, for
example, float operations. True if the function does not involve these
operations, false if it does. SQL Server uses this information for
indexing. |
FillRowMethodName | Used
for table-valued functions. This is the name of a method in the same
class that serves as the table-valued function contract. |
TableDefinition | Used for table-valued functions. Defines the column list in the table returned. |
Name | The name of the SQL function. |
Here
are a few examples of more complex user-defined functions. Look through
these functions to understand how they are implemented. When looking
through each function ask yourself if the function should be registered
with the permission set of SAFE, EXTERNAL_ACCESS, or UNSAFE.
Registering CLR Functions
You
must register CLR functions with a SQL Server database before they can
be used. To do this, first register the assembly containing the
function using CREATE ASSEMBLY. Then, use the CREATE FUNCTION statement with EXTERNAL NAME parameter to register the function for use. Use the syntax shown in Example 3 to register the function.
Example 3. Syntax—Registering CLR Functions
CREATE FUNCTION dbo.function_name (@param1 as param1_data_type, @param2 as param2_data_type) RETURNS return_data_type AS EXTERNAL NAME assembly_name.class_name.function_name;
|
Note that parameters and return types must be specified when registering the user-defined CLR function. In Examples 4 and 5 we will register the SayHello function we have created earlier in this article. The SayHello function takes no parameters, and returns a value of type SqlString. The SqlString type is equivalent of the nvarchar SQL Server data type.
Example 4. Registering a Simple CLR Function
CREATE FUNCTION dbo.SayHello() RETURNS nvarchar(max) AS EXTERNAL NAME MyAwesomeFunctions.UserDefinedFunctions.SayHello;
|
Example 5. Calling a Simple CLR Function
Select dbo.SayHello() --RESULTS: ------------- Hello world! (1 row(s) affected)
|
Creating CLR Aggregates
Aggregates
are functions that operate on a set of values and return a single
value. Examples of built-in aggregate functions are SUM, AVG, and MAX.
In your query, you will usually pass a column to these aggregate
functions and will be returned the total, average, or maximum value of
all the rows in that column. Previously, SQL Server supported only
built-in aggregates. Custom aggregate functionality was achieved using
cursors, which was rather cumbersome. With CLR integration, you can now
create your own custom aggregate functions. CLR integration is the only
way
to create custom aggregates in SQL Server. CLR aggregates are simpler,
and offer better performance than cursors. Performance of custom
aggregates compares to performance of built-in aggregates.
The
ability to create custom aggregates is very powerful. For example,
imagine being able to aggregate a list of binary photographs stored in
the database as an image data type and determine which one has the most
likeness to a specific photograph you pass to it. In the case of
spatial data, you can determine the total length of a route around all
those points, or the shortest route. You could also implement complex
statistical analysis with functions unavailable in SQL Server out of
the box, or you could simply concatenate string values from a varchar
column.
In order to create a user-defined aggregate, your class must adhere to the aggregation contract. The aggregation contract consists of the following requirements:
Mark your class with SqlUserDefinedAggregate.
Create an instance method called Init.
This method initializes the aggregation. Here, you will reset any
variables that may have been set in a previous use, and instantiate any
variables you will use during the aggregation.
Create an instance method called Accumulate.
It should accept one parameter: the value that is being accumulated.
SQL Server calls this method for every value in the group being
processed. You should use this method to update the state of your
aggregate as the value is accumulated.
Create an instance method called Merge. It should accept one parameter: another instance of your aggregate class. This method is used to merge multiple aggregations.
Create an instance method called Terminate. This method should accept no parameters, but it should return the final result of your aggregation.
Table 2 lists all properties that apply to the SqlUserDefinedAggregate custom attribute.
Table 2. SqlUserDefinedAggregate Custom Attribute Properties
Property | Explanation |
---|
Format | Format of serializing the aggregate to a string. This can be either native or user-defined. |
IsInvariantToDuplicates | Determines whether the function ignores duplicate values. For example, the MIN and MAX aggregates ignore duplicate values. The SUM
aggregate does not ignore duplicate values and will return a very
different result when the same value is passed to it multiple times. |
IsInvariantToNulls | Determines if the function ignores null values. For example, the SUM function will return the same value when null values are passed, while the COUNT function will not. |
IsInvariantToOrder | This property is not used by SQL Server at this point. It is reserved for future use. |
IsNullIfEmpty | Determines whether the aggregate will return null if no values have been passed to it. |
MaxByteSize | The
amount of memory in bytes your aggregate will consume during
processing. The maximum value is 2GB. Specify between 1 to 8000 bytes,
or–1 if your aggregate will consume over 8000 bytes (but less than 2GB). |
Examples 6 and 7
show how we can list text values all on one line, separating them by a
semicolon. This can be very useful when you have a column storing
e-mail addresses and you want to list them all on one line with
separators (and then perhaps send it to your e-mail system to send an
e-mail to all those addresses).
Example 6. Implementing ListWithSeparator—C#
using System; using System.Data; using Microsoft.SqlServer.Server; using System.Data.SqlTypes; using System.Text; using System.IO; [Serializable] [SqlUserDefinedAggregate( Format.UserDefined, IsInvariantToNulls = true, IsInvariantToDuplicates = true, IsInvariantToOrder = true, MaxByteSize = -1) ] public class ListWithSeparator : IBinarySerialize { // Comment: the tempStringBuilder variable holds the list while the aggregation is being processed public StringBuilder tempStringBuilder; public void Init() { tempStringBuilder = new StringBuilder(); } public void Accumulate(SqlString value) { if (!value.IsNull) { tempStringBuilder.Append(value.Value); tempStringBuilder.Append(";"); } } public void Merge(ListWithSeparator anotherList) { tempStringBuilder.Append(anotherList.tempStringBuilder.ToString()); } public SqlString Terminate() { if (tempStringBuilder != null && tempStringBuilder.Length > 0) { return new SqlString(tempStringBuilder.ToString()); } return new SqlString(string.Empty); } public void Read(BinaryReader reader) { tempStringBuilder = new StringBuilder(reader.ReadString()); } public void Write(BinaryWriter writer) { writer.Write(tempStringBuilder.ToString()); } }
|
Example 7. Implementing ListWithSeparator—Visual Basic .NET
Imports System Imports System.Data Imports Microsoft.SqlServer.Server Imports System.Data.SqlTypes Imports System.Text Imports System.IO <Serializable(), SqlUserDefinedAggregate(Format.UserDefined, IsInvariantToNulls:=True, IsInvariantToDuplicates:=True, IsInvariantToOrder:=True, MaxByteSize:=-1)> _ Public Class ListWithSeparator Implements IBinarySerialize Private tempStringBuilder As StringBuilder Public Sub Init() tempStringBuilder = New StringBuilder() End Sub Public Sub Accumulate(ByVal value As SqlString) If value.IsNull Then Return End If tempStringBuilder.Append(value.Value).Append(","c) End Sub Public Sub Merge(ByVal anotherList As ListWithSeparator) tempStringBuilder.Append(anotherList.tempStringBuilder) End Sub Public Function Terminate() As SqlString If Not (tempStringBuilder Is Nothing) AndAlso tempStringBuilder. Length > 0 Then return New SqlString(tempStringBuilder.ToString()) End If Return New SqlString(String.Empty) End Function Public Sub Read(reader As BinaryReader) tempStringBuilder = New StringBuilder(reader.ReadString()) End Sub Public Sub Write(writer As BinaryWriter) writer.Write(Me.intermediateResult.ToString()) End Sub End Class
|
Tip
Quickly revise the five requirements that make up a custom aggregate contract. These are: use the SqlUserDefinedAggregate attribute, and implement Init, Accumulate, Merge, and Terminate methods. You don’t need to memorize the actual syntax for doing this.